/*
 * DataType.java
 *
 * Created on July 31, 2007, 7:17 PM
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.variable.types;

import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import tal.drivechain.chain.ChainNode;
import tal.drivechain.matchers.ValueMatcher;
import tal.drivechain.variable.Variable;
import tal.drivechain.variable.expressions.CustomExpression;
import tal.drivechain.variable.expressions.SimpleExpression;

/**
 * The base class for any variable data type object. DataType objects are responsible
 * for creating and evaluating the value object of a variable as well as casting from
 * one data type to another.
 *
 * @author Tal Eisenberg
 */
public abstract class DataType implements Serializable {
    /** Values of variables of this data type must be object of this class. */
    protected Class valueClass; 
    /** The name of the data type */
    private String name; 
    /** An index number for the data type */
    private int typecode;
    
    public final static IntType INT = new IntType();
    public final static FloatType FLOAT = new FloatType();
    public final static ExprType EXPR = new ExprType();
    public final static BooleanType BOOLEAN = new BooleanType();
    public final static LongType LONG = new LongType();
    public final static MidiType MIDI = new MidiType();
    public final static StringType STRING = new StringType();
    public final static VarType VAR = new VarType();
    public final static CommandType COMMAND = new CommandType();
    public final static SetPropertyType PROPERTY = new SetPropertyType();
    public final static EventType EVENT = new EventType();
    public final static boolean MATCH_ARRAY = true;
    
    /** An array the contains each possible DataType subclass */
    public final static DataType[] types = new DataType[] {
        INT, FLOAT, EXPR, BOOLEAN, LONG, MIDI, STRING, VAR, COMMAND, PROPERTY, EVENT };

    public final static int TYPECODE_UNKNOWN = -1;
    
    /** Creates a new instance of a DataType.
     * 
     * @param name the name of the data type.
     * @param valueClass the class of the value object that variables using this data type must have.
     * @param typecode the index of the DataType in the system.
     */
    public DataType(String name, Class valueClass, int typecode) {
        this.name = name;
        this.valueClass = valueClass;
        this.typecode = typecode;
    }
    
    /**
     * Returns the name of the data type. 
     */
    public final String getName() { return name; }
    /**
     * Returns the value class of the data type.
     */
    public final Class getValueClass() { return valueClass; }
    
    /**
     * Returns an object according the the value definition string. The object should be
     * of valueClass class.
     */
    public abstract Object parseValue(String value) throws IllegalArgumentException;
    
    /**
     * Returns a String representation of a variable value. Unless the method is overriden
     * it returns value.toString() or "(null-value)" if value is null.
     */
    public String toString(Object value, ChainNode context) {
        if (value!=null) return value.toString(); 
        else return "(null-value)";
    }
    
    /**
     * Returns true if the input object's class matches the value class of this DataType.
     */
    public boolean checkType(Object value) {
        if (value==null) return true;
        return (value.getClass().equals(valueClass));
    }

    public static DataType findType(Class c) {
        if (c.equals(Float.class) || c.equals(float.class) || c.equals(double.class)) return FLOAT;
        else if (c.equals(Short.class) || c.equals(short.class) || c.equals(int.class)) return INT;
        else if (c.equals(long.class)) return LONG;

        for (int i=0; i<types.length; i++) {
            if (types[i].getValueClass().isAssignableFrom(c))
                return types[i];
        }

        return DataType.VAR;
    }

    /**
     * Returns a DataType object that can handle the input object's class as a value.
     *
     * @return the matching DataType object or null if there's no matching DataType.
     */
    public static DataType findType(Object value) {
        if (value!=null) {
            if (value instanceof List) {
                List listVal = (List)value;
                Class elementClass = null;
                for (int i=1; i<listVal.size(); i++) {
                    if (!listVal.get(i-1).getClass().equals(listVal.get(i).getClass())) {
                        elementClass = Object.class;
                        break;
                    } else elementClass = listVal.get(i).getClass();

                }
                
                if (elementClass==null) elementClass = Object.class;
                return new ArrayType(findType(elementClass));
            } else if (value instanceof HashMap) {
                HashMap<String, Object> mapVal = (HashMap<String,Object>)value;
                Class elementClass = null;
                Object lastElement = null;
                for (String key : mapVal.keySet()) {
                    Object element = mapVal.get(key);
                    if (lastElement!=null) {
                        if (!element.getClass().equals(lastElement.getClass())) {
                            elementClass = Object.class;
                            break;
                        } else elementClass = element.getClass();
                    }
                    lastElement = element;
                }

                if (elementClass==null) elementClass = Object.class;
                return new HashmapType(findType(elementClass));
            } else return findType(value.getClass());
        } else return DataType.EVENT;

    }

    public boolean isValue(String def) {
        try {
            this.parseValue(def);
            return true;
        } catch (IllegalArgumentException ie) {
            return false;
        }
    }

    public static DataType findTypeByDef(String def) throws IllegalArgumentException {
        if (STRING.isValue(def))
            return STRING;
        if (EVENT.isValue(def))
            return EVENT;
        if (INT.isValue(def))
            return INT;
        if (LONG.isValue(def))
            return LONG;
        if (FLOAT.isValue(def))
            return FLOAT;
        if (BOOLEAN.isValue(def))
            return BOOLEAN;
        if (MIDI.isValue(def))
            return MIDI;
        if (EXPR.isValue(def))
            return EXPR;
        if (COMMAND.isValue(def))
            return COMMAND;
        if (PROPERTY.isValue(def))
            return PROPERTY;
        throw new IllegalArgumentException("Can't find type for definition: " + def);
    }

    /**
     * Returns a DataType object by its name.
     */
    public static DataType parseType(String type) throws IllegalArgumentException {
        if (type==null) return null;
        if (type.endsWith("[]")) { // array
            DataType elementType = DataType.parseType(type.substring(0, type.length()-2));
            return new ArrayType(elementType);
        } else if (type.endsWith("[#]")) { // hashmap
            DataType elementType = DataType.parseType(type.substring(0, type.length()-3));
            return new HashmapType(elementType);
        } else {
            for (int i=0; i<types.length; i++) {
                if (type.equals(types[i].getName())) return types[i];
            }
            throw new IllegalArgumentException("Unknown data type: " + type);
        }
    }
    
    /**
     * Called when a value needs to be cast to this DataType, in case a different
     * value class is needed.
     */
    public Object cast(Object value) throws IllegalArgumentException {
        if (value==null) throw new IllegalArgumentException("Can't cast null value to " + getName());
        if (value.getClass().equals(this.getValueClass())) return value;
        else if (value instanceof String)
            return this.parseValue((String)value);
        else throw new IllegalArgumentException("Can't cast " + value + " to " + getName());
    }
    
    /**
     * Called when a Variable needs to evaluate its value. Allows for data type
     * specific evaluation methods.
     */
    public Object extractValue(Object value, ChainNode context) throws IllegalArgumentException {
        return value;
    }
    
    public CustomExpression createExpression(String value) throws IllegalArgumentException {
        return new SimpleExpression(value, this);
    }
    
    public abstract ValueMatcher createValueMatcher() throws IllegalArgumentException;
    
    /**
     * Returns the type code of this DataType
     */
    public final int getTypeCode() { return typecode; }
    
    /**
     * Returns true if DataType d is the same as this DataType.
     *
     * @param d the DataType to compare.
     * @param array If true the method checks if the input DataType is an array
     *        of this DataType.
     */
    public final boolean matches(DataType d, boolean array) {
        return (d.getTypeCode()+(array ? 100 : 0)==this.getTypeCode());
    }
    
    /**
     * Returns true if DataType d has the same type code as this DataType
     */
    public final boolean matches(DataType d) {
        return (d.getTypeCode()==this.getTypeCode());
    }
    
    /**
     * 
     * @return true if this DataType is an array of some sort.
     */
    public final boolean isArray() {
        return (this.getTypeCode()>=100);
    }

    public Variable evaluateVar(Variable var, ChainNode context) {
        return var;
    }
    
    /**
     * Creates a cast error IllegalArgumentException when Object value can't
     * be cast to DataType dest.
     *
     * @returns an IllegalArgumentException that contains the message for this value and 
     *          data type.
     */
    static IllegalArgumentException castError(Object value, DataType dest) {
        DataType valueType = findType(value);
        String sValueType;
        if (valueType==null) sValueType = value.getClass().getName();
        else sValueType = valueType.getName();
        return new IllegalArgumentException(
                "Can't cast a " + sValueType + " value to " + dest.getName());
    }
}
